namespace Radar
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    internal static class ExpressionHelper
    {
        private static readonly MethodInfo genericResolveMethod;

        static ExpressionHelper()
        {
            genericResolveMethod = typeof (IContainer).GetMethod("Resolve", new[] {typeof (ResolveContext)});
        }

        internal static Func<ResolveContext, object> BuildConstructor(Type typeToBuild, IDictionary<string, Func<object>> parameterBuilders, IContainer container)
        {
            if (typeToBuild == null) throw new ArgumentNullException("typeToBuild");
            if (parameterBuilders == null) throw new ArgumentNullException("parameterBuilders");
            if (container == null) throw new ArgumentNullException("container");

            if (typeToBuild.IsAbstract || typeToBuild.IsInterface || typeToBuild.IsGenericTypeDefinition)
            {
                var message = string.Format("'{0}' is either an abstract class, interface or generic type definition", typeToBuild);
                throw new InstanceCreationException(message);
            }

            var constructor = FindSuitableConstructor(typeToBuild);
            var contextParameter = Expression.Parameter(typeof(ResolveContext), "context");
            var parameters = BuildParameterExpressions(constructor, parameterBuilders, container, contextParameter);

            return Expression
                .Lambda<Func<ResolveContext, object>>(Expression.New(constructor, parameters), contextParameter)
                .Compile();
        }

        private static Expression[] BuildParameterExpressions(ConstructorInfo constructor,
            IDictionary<string, Func<object>> parameterBuilders,
            IContainer container,
            ParameterExpression contextParameter)
        {
            return constructor
                .GetParameters()
                .Select(x => GetParameterBuilder(x, parameterBuilders, container, contextParameter))
                .ToArray();
        }

        private static Expression GetParameterBuilder(ParameterInfo parameterInfo,
            IDictionary<string, Func<object>> parameterBuilders,
            IContainer container,
            ParameterExpression contextParameter)
        {
            Func<object> builder;
            Expression constructorExpression;

            if (!parameterBuilders.TryGetValue(parameterInfo.Name, out builder))
            {
                var instance = Expression.Constant(container);

                var ctor = genericResolveMethod.MakeGenericMethod(parameterInfo.ParameterType);
                constructorExpression = Expression.Call(instance, ctor, contextParameter);
            }
            else if (builder.Method.IsStatic)
            {
                constructorExpression = Expression.Convert(Expression.Call(builder.Method), parameterInfo.ParameterType);
            }
            else
            {
                var builderCall = Expression.Call(Expression.Constant(builder.Target), builder.Method);
                constructorExpression = Expression.Convert(builderCall, parameterInfo.ParameterType);
            }

            return constructorExpression;
        }

        private static ConstructorInfo FindSuitableConstructor(Type type)
        {
            var constructors = type.GetConstructors();
            if (constructors.Length == 0)
            {
                var message = string.Format("Type '{0}' doesn't have public constructors", type);
                throw new InstanceCreationException(message);
            }

            return constructors.First();
        }
    }
}